package edu.arizona.sista.learn.evaluation;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import edu.arizona.sista.learn.activity.model.ActivityInstance;
import edu.arizona.sista.learn.activity.model.Bindings;
import edu.arizona.sista.learn.activity.model.PredicateInterval;
import edu.arizona.sista.learn.activity.model.args.PredObject;
import edu.arizona.sista.learn.activity.util.KnownPredicates;

public class InverseDistancesMetric implements Metric {

	private double _distanceExponent = 1;
	private boolean _calcTrueNegs = false;
	private boolean _weightByNumFrames = true;
	
	public InverseDistancesMetric() {
		this(null, null, null);
	}
	
	public InverseDistancesMetric(Double distanceExponent, Boolean calcTrueNegs, Boolean weightByNumFrames) {
		if( distanceExponent != null )
			_distanceExponent = distanceExponent;
		if( calcTrueNegs != null )
			_calcTrueNegs = calcTrueNegs;
		if( weightByNumFrames != null )
			_weightByNumFrames = weightByNumFrames;
	}
	
	@Override
	public Score evaluate(ActivityInstance testee, Bindings binding,
			ActivityInstance standard) {
		
		final double maxReward = 1.0; //TODO: make this an argument??
		
		//To aggregate fluents of same predicate WITH same arguments
		Map<String, List<PredicateInterval>> boundPredicateMap = new HashMap<String, List<PredicateInterval>>();
		
		//And keep track of their score with regards to each gold fluent of the same type
		Map<String, List<List<Double>>> scoreMap = new HashMap<String, List<List<Double>>>();
		
		//Also gonna find total score per bound predicate in standard
		Map<String, Double> totalScoreMap = new HashMap<String, Double>();
		
		/* First let's score each detected fluent 
		 * with each potentially matching standard fluent 
		 */

		//iterate through detected fluents
		for( String predicateName : testee.getIntervalsMap().keySet() ) {
			for( PredicateInterval testPInterval : testee.getIntervals( predicateName ) ){
				
				if( binding != null)
					testPInterval.getPredicate().bindArguments(binding);
				
				//Initialize data structures
				String boundPredicateName = testPInterval.getPredicate().toString();
				System.out.println( boundPredicateName );
				if(  ! boundPredicateMap.containsKey( boundPredicateName )  ) { 
					boundPredicateMap.put( boundPredicateName, new ArrayList<PredicateInterval>() );
					scoreMap.put( boundPredicateName, new ArrayList<List<Double>>() );
				}
				//this interval is gonna get some scores (maybe)
				boundPredicateMap.get( boundPredicateName ).add( testPInterval );
				//hold this detected fluent's scores with regards to each gold fluent of same type
				List<Double> scoreList = new ArrayList<Double>();
				scoreMap.get( boundPredicateName ).add( scoreList );
				
				//are there any gold fluents of this type?
				if(  standard.getIntervalsMap().containsKey( predicateName )  ) {
					
					//then let's iterate through them
					for(  PredicateInterval standardPInterval : standard.getIntervals( predicateName )  ) {
						
						//check to see if args are the same, according to the binding given
						if(  testPInterval.getPredicate()
								.bindableTo( standardPInterval.getPredicate(), binding )  ) {
							
							//predicate's same and args are same so score it
							scoreList.add(  score( testPInterval, standardPInterval )  );
							
						}
					}
				}
			}
		}
		
		/* Now let's iterate through the standard fluents
		 * 
		 * each standard can only give out a certain amount of reward
		 * so we'll iterate through the scores of matching detections,
		 * start with the biggest scores, count how much reward we're
		 * giving, and zero out the scores of the rest 
		 */
		double totalFalseNegativeFluents = 0;
		double totalFalseNegativeFrames = 0;
		
		double totalNumGoldFluents = 0;
		double totalNumGoldFrames = 0;
		
		Map<String, Integer> standardFluentCounts = new HashMap<String, Integer>();
		Map<String, Double> standardFrameCounts = new HashMap<String, Double>();
		for( String predicateName : standard.getIntervalsMap().keySet() ) {
			
			//Gotta do this by index, so we can index into matching detections' score list too
			List<PredicateInterval> standardPIntervals = standard.getIntervals( predicateName );
			for( int i=0; i < standardPIntervals.size(); i++ ){
				
				PredicateInterval standardPInterval = standardPIntervals.get( i );
				String boundPredicateName = standardPInterval.getPredicate().toString();
				System.out.println( boundPredicateName );
				
				double length = standardPInterval.end - standardPInterval.start;
				if(  ! standardFrameCounts.containsKey( boundPredicateName )  ) {
					standardFluentCounts.put(predicateName, 1);
					standardFrameCounts.put(predicateName, length);
				}
				else {
					Integer thing1 = standardFluentCounts.get( boundPredicateName ); 
					thing1 += 1;
					
					System.out.println( "============ " + standardFrameCounts.get( boundPredicateName ) );
					Double thing2 = standardFrameCounts.get( boundPredicateName ); 
					thing2 += length;
					System.out.println( "============ " + standardFrameCounts.get( boundPredicateName ) );
				}
				
				totalNumGoldFluents += 1;
				totalNumGoldFrames += standardPInterval.end - standardPInterval.start;
				
				double rewardAllocated = 0;
				
				//make a shallow copy of the scoreListList so we can remove scoreLists,
				// but still modify the scores in the remaining scoreLists
				// ...I know.. it's confusing
				List<List<Double>> scoreListListCopy = new ArrayList<List<Double>>();
				if(  scoreMap.containsKey( boundPredicateName )  )
					scoreListListCopy = new ArrayList<List<Double>>(  scoreMap.get( boundPredicateName )  );
				
				while( rewardAllocated < maxReward  && scoreListListCopy.size() > 0){
				
					double maxScore = 0;
					List<Double> maxList = null;
					//Each list corresponds to a testPInterval from above, the ith score on the list
					// was his score against this standardPInterval
					for( List<Double> testScores : scoreListListCopy ) {
						
						if(  testScores.get( i ) > maxScore ){
							maxScore = testScores.get( i );
							maxList = testScores;
						}
						
					}
					
					//found the max
					if( maxList != null ) {
						
						double remainingReward = maxReward - rewardAllocated; 
						if( maxScore > remainingReward ) {
							rewardAllocated = maxReward;
							maxList.set( i, remainingReward );
						}
						else
							rewardAllocated += maxScore;
						
						scoreListListCopy.remove( maxList );
						
					}
				}//while( rewardAllocated < maxReward )
				
				//If there were no detections, we'll need to count false negatives here
				if( rewardAllocated == 0 ) {
					totalFalseNegativeFluents += 1;
					totalFalseNegativeFrames += length;
				}
					
				
				//all reward has been allocated, zero out the remainder
				for( List<Double> testScores : scoreListListCopy ) {
					testScores.set( i , 0.0 );
				}
				
			}//for
		}//for
		
		/* Okay, now let's iterate through the detections again
		 * and add up true positive, false positive and false negative scores
		 * 
		 * TODO false negative, true negative
		 */
		double totalTruePositiveFluents = 0;
		double totalTruePositiveFrames = 0;
		
		double totalFalsePositiveFluents = 0;
		double totalFalsePositiveFrames = 0;
		
		double totalNumDetectionFluents = 0;
		double totalNumDetectionFrames = 0;
		for( String boundPredicateName : boundPredicateMap.keySet() ) {
			
			double numDetectionFluents = 0;
			double numDetectionFrames = 0;
			
			double truePositiveFluents = 0;
			double truePositiveFrames = 0;
			
			for( PredicateInterval testPInterval : boundPredicateMap.get( boundPredicateName ) ){
				
				numDetectionFluents += 1;
				int intervalFrames = testPInterval.end - testPInterval.start;
				numDetectionFrames += intervalFrames;
				
				double allScore = 0;
				for(  List<Double> scores : scoreMap.get( boundPredicateName )  ){
					
					for( Double score : scores )
						allScore += score;
					
					truePositiveFluents += allScore;
					truePositiveFrames += allScore * intervalFrames;
				}
			}
			
			totalNumDetectionFluents += numDetectionFluents;
			totalNumDetectionFrames += numDetectionFrames;
			
			double falsePositiveFluents = numDetectionFluents - truePositiveFluents;
			double falsePositiveFrames = numDetectionFrames - truePositiveFrames;
			
			double falseNegativeFluents = 0;
			double falseNegativeFrames = 0;
			if(  standardFluentCounts.containsKey( boundPredicateName )  ) {
				falseNegativeFluents = standardFluentCounts.get( boundPredicateName ) - truePositiveFluents;
				falseNegativeFrames = standardFrameCounts.get( boundPredicateName ) - truePositiveFrames;
			}
			
			totalTruePositiveFluents += truePositiveFluents;
			totalFalsePositiveFluents += falsePositiveFluents;
			totalFalseNegativeFluents += falseNegativeFluents;
			
			totalTruePositiveFrames += truePositiveFrames;
			totalFalsePositiveFrames += falsePositiveFrames;
			totalFalseNegativeFrames += falseNegativeFrames;
			
		}
		
		double precision;
		double recall;
		if( _weightByNumFrames ) {
			precision = totalTruePositiveFrames / totalNumDetectionFrames;
			recall = totalTruePositiveFrames / totalNumGoldFrames;
		}
		else {
			precision = totalTruePositiveFluents / totalNumDetectionFluents;
			recall = totalTruePositiveFluents / totalNumGoldFluents;
		}
		
		double f1 = 2 * precision * recall / (precision + recall);
		
		System.out.println( precision + " " + recall + " " + f1);
		return new InverseDistanceScore( f1 );
	}
	
	//returns what proportion of standard's reward testee could collect
	private double score(PredicateInterval testee, PredicateInterval standard) {
		
		double testMid = (testee.end - testee.start)/2.0;
		double stanMid = (standard.end - standard.start)/2.0; 
		
		double startScore = 10 / (3 * (10 + Math.pow( Math.abs(testee.start - standard.start), _distanceExponent )  ));
		double midScore = 10 / (3 * (10 + Math.pow( Math.abs(testMid - stanMid), _distanceExponent )  ));
		double endScore = 10 / (3 * (10 + Math.pow( Math.abs(testee.end - standard.end), _distanceExponent )  ));
		
		System.out.println( testee + " and " + standard + " scored " + startScore + midScore + endScore);
		return startScore + midScore + endScore;
		
	}

	private static void test(){
		
		InverseDistancesMetric metric;
		InverseDistanceScore score;
		
		metric = new InverseDistancesMetric(2.0, null, null);
		
		List<PredicateInterval> testee1 = new ArrayList<PredicateInterval>();
		testee1.add(   new PredicateInterval(  KnownPredicates.moving.recastCopy( new PredObject(null, "Person 1") ),
				10, 20));
		
		List<PredicateInterval> standard1 = new ArrayList<PredicateInterval>();
		standard1.add(   new PredicateInterval(  KnownPredicates.moving.recastCopy( new PredObject(null, "Person 1") ),
				10, 20));
		
		score  = (InverseDistanceScore) metric.evaluate(  new ActivityInstance( testee1 ), 
				null,
				new ActivityInstance( standard1 )  );
		
		System.out.println( score._f1 );
		
		
		
		
		
		List<PredicateInterval> testee2 = new ArrayList<PredicateInterval>();
		testee2.add(   new PredicateInterval(  KnownPredicates.moving.recastCopy( new PredObject(null, "Person 1") ),
				11, 21));
		
		score  = (InverseDistanceScore) metric.evaluate(  new ActivityInstance( testee2 ), 
				null,
				new ActivityInstance( standard1 )  );
		
		System.out.println( score._f1 );
		
		
		
		
		List<PredicateInterval> testee3 = new ArrayList<PredicateInterval>();
		testee3.add(   new PredicateInterval(  KnownPredicates.moving.recastCopy( new PredObject(null, "Person 1") ),
				15, 25));
		
		score  = (InverseDistanceScore) metric.evaluate(  new ActivityInstance( testee3 ), 
				null,
				new ActivityInstance( standard1 )  );
		
		System.out.println( score._f1 );
		
		
		
		
		List<PredicateInterval> testee4 = new ArrayList<PredicateInterval>();
		testee4.add(   new PredicateInterval(  KnownPredicates.moving.recastCopy( new PredObject(null, "Person 1") ),
				20, 30));
		
		score  = (InverseDistanceScore) metric.evaluate(  new ActivityInstance( testee4 ), 
				null,
				new ActivityInstance( standard1 )  );
		
		System.out.println( score._f1 );
		
		
		
		
		List<PredicateInterval> testee5 = new ArrayList<PredicateInterval>();
		testee5.add(   new PredicateInterval(  KnownPredicates.moving.recastCopy( new PredObject(null, "Person 1") ),
				0, 30));
		
		score  = (InverseDistanceScore) metric.evaluate(  new ActivityInstance( testee5 ), 
				null,
				new ActivityInstance( standard1 )  );
		
		System.out.println( score._f1 );
		
	}
	
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		
		test();

	}

}